﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IP2LocationParser
{
    public class IPResult
    {
        public System.Net.IPAddress IPAddressRangeFrom;
        public System.Net.IPAddress IPAddressRangeTo;

        public string CountryShort;
        public string CountryLong;
        public string Region;
        public string City;
        public string InternetServiceProvider;
        public string DomainName;
        public string ZipCode;
        public string NetSpeed;
        public string IDDCode;
        public string AreaCode;
        public string WeatherStationCode;
        public string WeatherStationName;
        public string TimeZone;
        public float? Latitude;
        public float? Longitude;
        public string MCC;
        public string MNC;
        public string MobileBrand;
    }

    public class IP2LocationParser : IDisposable
    {
        readonly System.IO.Stream _stream;
        readonly bool _ownStream;
        readonly DBHeader _header;
        readonly FieldStructure _structure;

        readonly Lazy<long> _addrLowerBound;
        readonly Lazy<long> _addrUpperBound;

        #region types

        private sealed class DBHeader
        {
            public readonly int _DBType;
            public readonly int _DBColumn;
            public readonly int _DBYear;
            public readonly int _DBMonth;
            public readonly int _DBDay;
            public readonly int _DBCount;
            public readonly int _BaseAddr;

            public DBHeader(System.IO.Stream stream)
            {
                this._DBType = stream.ReadByte();
                this._DBColumn = stream.ReadByte();
                this._DBYear = stream.ReadByte();
                this._DBMonth = stream.ReadByte();
                this._DBDay = stream.ReadByte();

                byte[] buffer = new byte[4];
                stream.Read(buffer, 0, buffer.Length);
                this._DBCount = BitConverter.ToInt32(buffer, 0);
                stream.Read(buffer, 0, buffer.Length);
                this._BaseAddr = BitConverter.ToInt32(buffer, 0) - 1;
            }
        }

        [Flags]
        public enum Fields : int
        {
            None = 0,
            CountryShort = 1,
            CountryLong = 2,
            Region = 4,
            City = 8,
            InternetServiceProvider = 16,
            Latitude = 32,
            Longitude = 64,
            DomainName = 128,
            ZipCode = 256,
            TimeZone = 512,
            NetSpeed = 1024,
            IDDCode = 2048,
            AreaCode = 4096,
            WeatherStationCode = 8192,
            WeatherStationName = 16384,
            MCC = 32768,
            MNC = 65536,
            MobileBrand = 131072,
            All = 0x7fffffff
        };

        public class FieldStructure
        {
            public int? COUNTRY_POSITION;
            public int? REGION_POSITION;
            public int? CITY_POSITION;
            public int? ISP_POSITION;
            public int? LATITUDE_POSITION;
            public int? LONGITUDE_POSITION;
            public int? DOMAIN_POSITION;
            public int? ZIPCODE_POSITION;
            public int? TIMEZONE_POSITION;
            public int? NETSPEED_POSITION;
            public int? IDDCODE_POSITION;
            public int? AREACODE_POSITION;
            public int? WEATHERSTATIONCODE_POSITION;
            public int? WEATHERSTATIONNAME_POSITION;
            public int? MCC_POSITION;
            public int? MNC_POSITION;
            public int? MOBILEBRAND_POSITION;
        }

        private static readonly FieldStructure[] _predefinedStructure = new FieldStructure[21] {
            new FieldStructure {},
            new FieldStructure {COUNTRY_POSITION = 1},
            new FieldStructure {COUNTRY_POSITION = 1,ISP_POSITION = 2},
            new FieldStructure {COUNTRY_POSITION = 1,REGION_POSITION = 2,CITY_POSITION = 3},
            new FieldStructure {COUNTRY_POSITION = 1,REGION_POSITION = 2,CITY_POSITION = 3,ISP_POSITION = 4},
            new FieldStructure {COUNTRY_POSITION = 1,REGION_POSITION = 2,CITY_POSITION = 3,LATITUDE_POSITION = 4,LONGITUDE_POSITION = 5},
            new FieldStructure {COUNTRY_POSITION = 1,REGION_POSITION = 2,CITY_POSITION = 3,ISP_POSITION = 6,LATITUDE_POSITION = 4,LONGITUDE_POSITION = 5},
            new FieldStructure {COUNTRY_POSITION = 1,REGION_POSITION = 2,CITY_POSITION = 3,ISP_POSITION = 4,DOMAIN_POSITION = 5},
            new FieldStructure {COUNTRY_POSITION = 1,REGION_POSITION = 2,CITY_POSITION = 3,ISP_POSITION = 6,LATITUDE_POSITION = 4,LONGITUDE_POSITION = 5,DOMAIN_POSITION = 7},
            new FieldStructure {COUNTRY_POSITION = 1,REGION_POSITION = 2,CITY_POSITION = 3,LATITUDE_POSITION = 4,LONGITUDE_POSITION = 5,ZIPCODE_POSITION = 6},
            new FieldStructure {COUNTRY_POSITION = 1,REGION_POSITION = 2,CITY_POSITION = 3,ISP_POSITION = 7,LATITUDE_POSITION = 4,LONGITUDE_POSITION = 5,DOMAIN_POSITION = 8,ZIPCODE_POSITION = 6},
            new FieldStructure {COUNTRY_POSITION = 1,REGION_POSITION = 2,CITY_POSITION = 3,LATITUDE_POSITION = 4,LONGITUDE_POSITION = 5,ZIPCODE_POSITION = 6,TIMEZONE_POSITION = 7},
            new FieldStructure {COUNTRY_POSITION = 1,REGION_POSITION = 2,CITY_POSITION = 3,ISP_POSITION = 8,LATITUDE_POSITION = 4,LONGITUDE_POSITION = 5,DOMAIN_POSITION = 9,ZIPCODE_POSITION = 6,TIMEZONE_POSITION = 7},
            new FieldStructure {COUNTRY_POSITION = 1,REGION_POSITION = 2,CITY_POSITION = 3,LATITUDE_POSITION = 4,LONGITUDE_POSITION = 5,TIMEZONE_POSITION = 6,NETSPEED_POSITION = 7},
            new FieldStructure {COUNTRY_POSITION = 1,REGION_POSITION = 2,CITY_POSITION = 3,ISP_POSITION = 8,LATITUDE_POSITION = 4,LONGITUDE_POSITION = 5,DOMAIN_POSITION = 9,ZIPCODE_POSITION = 6,TIMEZONE_POSITION = 7,NETSPEED_POSITION = 10},
            new FieldStructure {COUNTRY_POSITION = 1,REGION_POSITION = 2,CITY_POSITION = 3,LATITUDE_POSITION = 4,LONGITUDE_POSITION = 5,ZIPCODE_POSITION = 6,TIMEZONE_POSITION = 7,IDDCODE_POSITION = 8,AREACODE_POSITION = 9},
            new FieldStructure {COUNTRY_POSITION = 1,REGION_POSITION = 2,CITY_POSITION = 3,ISP_POSITION = 8,LATITUDE_POSITION = 4,LONGITUDE_POSITION = 5,DOMAIN_POSITION = 9,ZIPCODE_POSITION = 6,TIMEZONE_POSITION = 7,NETSPEED_POSITION = 10,IDDCODE_POSITION = 11,AREACODE_POSITION = 12},
            new FieldStructure {COUNTRY_POSITION = 1,REGION_POSITION = 2,CITY_POSITION = 3,LATITUDE_POSITION = 4,LONGITUDE_POSITION = 5,TIMEZONE_POSITION = 6,NETSPEED_POSITION = 7,WEATHERSTATIONCODE_POSITION = 8,WEATHERSTATIONNAME_POSITION = 9},
            new FieldStructure {COUNTRY_POSITION = 1,REGION_POSITION = 2,CITY_POSITION = 3,ISP_POSITION = 8,LATITUDE_POSITION = 4,LONGITUDE_POSITION = 5,DOMAIN_POSITION = 9,ZIPCODE_POSITION = 6,TIMEZONE_POSITION = 7,NETSPEED_POSITION = 10,IDDCODE_POSITION = 11,AREACODE_POSITION = 12,WEATHERSTATIONCODE_POSITION = 13,WEATHERSTATIONNAME_POSITION = 14},
            new FieldStructure {COUNTRY_POSITION = 1,REGION_POSITION = 2,CITY_POSITION = 3,ISP_POSITION = 6,LATITUDE_POSITION = 4,LONGITUDE_POSITION = 5,DOMAIN_POSITION = 7,MCC_POSITION = 8,MNC_POSITION = 9,MOBILEBRAND_POSITION = 10},
            new FieldStructure {COUNTRY_POSITION = 1,REGION_POSITION = 2,CITY_POSITION = 3,ISP_POSITION = 8,LATITUDE_POSITION = 4,LONGITUDE_POSITION = 5,DOMAIN_POSITION = 9,ZIPCODE_POSITION = 6,TIMEZONE_POSITION = 7,NETSPEED_POSITION = 10,IDDCODE_POSITION = 11,AREACODE_POSITION = 12,WEATHERSTATIONCODE_POSITION = 13,WEATHERSTATIONNAME_POSITION = 14,MCC_POSITION = 15,MNC_POSITION = 16,MOBILEBRAND_POSITION = 17}
        };

        #endregion types


        public IP2LocationParser(System.IO.Stream stream, bool ownStream = true, FieldStructure customStructure = null)
        {
            _stream = stream;
            _ownStream = ownStream;
            _header = new DBHeader(stream);
            if (customStructure == null)
                _structure = _predefinedStructure[_header._DBType];
            else
                _structure = customStructure;

            _addrLowerBound = new Lazy<long>(this.GetLowIPValue, false);
            _addrUpperBound = new Lazy<long>(this.GetHighIPValue, false);
        }

        public IPResult LocateIP(System.Net.IPAddress addr, Fields fields = Fields.All)
        {
            long addrTarget = BitConverter.ToUInt32(addr.GetAddressBytes().Reverse().ToArray(), 0);

            long low = 0;
            long hi = _header._DBCount - 1;
            long addrLow = _addrLowerBound.Value;
            long addrHi = _addrUpperBound.Value;

            if (addrTarget < addrLow || addrTarget >= addrHi)
                return null;

            while (low < hi)
            {
                if (low == hi - 1 && addrTarget >= addrLow && addrTarget < addrHi)
                {
                    var result = ReadEntry(low, fields);
                    result.IPAddressRangeTo = new System.Net.IPAddress(BitConverter.GetBytes((uint)addrHi).Reverse().ToArray());
                    return result;
                }

                long current = (low + hi) / 2;

                long addrCurrent = ReadAddressAt(current);

                if (addrTarget < addrCurrent)
                {
                    hi = current;
                    addrHi = addrCurrent;
                    continue;
                }
                if (addrTarget >= addrCurrent)
                {
                    low = current;
                    addrLow = addrCurrent;
                    continue;
                }
            }

            return null;
        }

        public IEnumerable<IPResult> ListEntries(Fields fields = Fields.All)
        {
            IPResult prev = null;
            for (long i = 0; i < _header._DBCount; i++)
            {
                if (prev == null)
                    prev = ReadEntry(i, fields);
                else
                {
                    var tmp = prev;
                    prev = ReadEntry(i, fields);
                    tmp.IPAddressRangeTo = prev.IPAddressRangeFrom;
                    yield return tmp;
                }
            }
        }


        #region privates

        private IPResult ReadEntry(long entry, Fields fields)
        {
            long position = _header._BaseAddr + entry * _header._DBColumn * 4;

            byte[] buffer = new byte[_header._DBColumn * 4];
            _stream.Seek(position, System.IO.SeekOrigin.Begin);
            _stream.Read(buffer, 0, buffer.Length);

            IPResult result = new IPResult();
            result.IPAddressRangeFrom = new System.Net.IPAddress(buffer.Take(4).Reverse().ToArray());

            if (_structure.COUNTRY_POSITION.HasValue)
            {
                long pos = BitConverter.ToUInt32(buffer, 4 * _structure.COUNTRY_POSITION.Value);
                if (fields.HasFlag(Fields.CountryShort))
                    result.CountryShort = ReadString(pos);
                if (fields.HasFlag(Fields.CountryLong))
                    result.CountryLong = ReadString(pos + 3);
            }
            if (_structure.REGION_POSITION.HasValue && fields.HasFlag(Fields.Region))
                result.Region = ReadString(BitConverter.ToUInt32(buffer, 4 * _structure.REGION_POSITION.Value));
            if (_structure.CITY_POSITION.HasValue && fields.HasFlag(Fields.City))
                result.City = ReadString(BitConverter.ToUInt32(buffer, 4 * _structure.CITY_POSITION.Value));
            if (_structure.ISP_POSITION.HasValue && fields.HasFlag(Fields.InternetServiceProvider))
                result.InternetServiceProvider = ReadString(BitConverter.ToUInt32(buffer, 4 * _structure.ISP_POSITION.Value));
            if (_structure.DOMAIN_POSITION.HasValue && fields.HasFlag(Fields.DomainName))
                result.DomainName = ReadString(BitConverter.ToUInt32(buffer, 4 * _structure.DOMAIN_POSITION.Value));
            if (_structure.ZIPCODE_POSITION.HasValue && fields.HasFlag(Fields.ZipCode))
                result.ZipCode = ReadString(BitConverter.ToUInt32(buffer, 4 * _structure.ZIPCODE_POSITION.Value));
            if (_structure.LATITUDE_POSITION.HasValue && fields.HasFlag(Fields.Latitude))
                result.Latitude = BitConverter.ToSingle(buffer, 4 * _structure.LATITUDE_POSITION.Value);
            if (_structure.LONGITUDE_POSITION.HasValue && fields.HasFlag(Fields.Longitude))
                result.Longitude = BitConverter.ToSingle(buffer, 4 * _structure.LONGITUDE_POSITION.Value);
            if (_structure.TIMEZONE_POSITION.HasValue && fields.HasFlag(Fields.TimeZone))
                result.TimeZone = ReadString(BitConverter.ToUInt32(buffer, 4 * _structure.TIMEZONE_POSITION.Value));
            if (_structure.NETSPEED_POSITION.HasValue && fields.HasFlag(Fields.NetSpeed))
                result.NetSpeed = ReadString(BitConverter.ToUInt32(buffer, 4 * _structure.NETSPEED_POSITION.Value));
            if (_structure.IDDCODE_POSITION.HasValue && fields.HasFlag(Fields.IDDCode))
                result.IDDCode = ReadString(BitConverter.ToUInt32(buffer, 4 * _structure.IDDCODE_POSITION.Value));
            if (_structure.AREACODE_POSITION.HasValue && fields.HasFlag(Fields.AreaCode))
                result.AreaCode = ReadString(BitConverter.ToUInt32(buffer, 4 * _structure.AREACODE_POSITION.Value));
            if (_structure.WEATHERSTATIONCODE_POSITION.HasValue && fields.HasFlag(Fields.WeatherStationCode))
                result.WeatherStationCode = ReadString(BitConverter.ToUInt32(buffer, 4 * _structure.WEATHERSTATIONCODE_POSITION.Value));
            if (_structure.WEATHERSTATIONNAME_POSITION.HasValue && fields.HasFlag(Fields.WeatherStationName))
                result.WeatherStationName = ReadString(BitConverter.ToUInt32(buffer, 4 * _structure.WEATHERSTATIONNAME_POSITION.Value));
            if (_structure.MCC_POSITION.HasValue && fields.HasFlag(Fields.MCC))
                result.MCC = ReadString(BitConverter.ToUInt32(buffer, 4 * _structure.MCC_POSITION.Value));
            if (_structure.MNC_POSITION.HasValue && fields.HasFlag(Fields.MNC))
                result.MNC = ReadString(BitConverter.ToUInt32(buffer, 4 * _structure.MNC_POSITION.Value));
            if (_structure.MOBILEBRAND_POSITION.HasValue && fields.HasFlag(Fields.MobileBrand))
                result.MobileBrand = ReadString(BitConverter.ToUInt32(buffer, 4 * _structure.MOBILEBRAND_POSITION.Value));

            return result;
        }

        private UInt32 ReadUInt32()
        {
            byte[] buffer = new byte[4];
            _stream.Read(buffer, 0, buffer.Length);
            return BitConverter.ToUInt32(buffer, 0);
        }

        private string ReadString(long position)
        {
            _stream.Seek(position, System.IO.SeekOrigin.Begin);
            int length = _stream.ReadByte();
            if (length >= 0)
            {
                byte[] buffer = new byte[length];
                _stream.Read(buffer, 0, length);
                return Encoding.Default.GetString(buffer);
            }
            return null;
        }

        private long ReadAddressAt(long entryIdx)
        {
            _stream.Seek(_header._BaseAddr + entryIdx * _header._DBColumn * 4, System.IO.SeekOrigin.Begin);
            return ReadUInt32();
        }

        private long GetLowIPValue()
        {
            return ReadAddressAt(0);
        }

        private long GetHighIPValue()
        {
            return ReadAddressAt(_header._DBCount - 1);
        }

        #endregion privates

        #region IDisposable Members

        bool _disposed = false;

        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    if (_stream != null)
                        _stream.Dispose();
                }
                _disposed = true;
            }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        #endregion
    }
}
